/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/graphics.hpp>

#include <baja/graphics/scenes/scenes_fwd.hpp>
#include <baja/graphics/scenes/scenes_interfaces.hpp>
#include <baja/graphics/scenes/details/scene_node_base.hpp>

namespace baja { namespace graphics { namespace scenes {

namespace d = ::baja::graphics::scenes::details;

// billboard_node implementation
class billboard_node : public d::scene_node_base<ibillboard_node>
{
public:
    billboard_node() : _width(1.f), _height(1.f), _horzTileCount(1), _vertTileCount(1)
    {
    }

    static std::shared_ptr<billboard_node> create(
        const std::shared_ptr<iengine> graphics,
        const float32 width, 
        const float32 height, 
        const uint32 horzTileCount,
        const uint32 vertTileCount
        )
    {
        std::shared_ptr<billboard_node> returnValue = std::make_shared<billboard_node>();
        returnValue->init(graphics, width, height, horzTileCount, vertTileCount);
        return returnValue;
    }
    
    // gets the size of the billboard
    void getSize(
        float32* width, 
        float32* height
        )
    {
        BAJA_CHECK_ARG(width);
        BAJA_CHECK_ARG(height);

        *width = _width;
        *height = _height;
    }
    
    // sets the size of this billboard in width/height
    void setSize(
        const std::shared_ptr<iengine>& graphics, 
        const float32 width,
        const float32 height
        )
    {
        // remember texture we are using
        std::shared_ptr<itexture> currentTexture;
        currentTexture = this->getTexture();

        // rebuild the billboard's mesh
        this->init(graphics, width, height, _horzTileCount, _vertTileCount);

        // restore texture to the billboard
        this->setTexture(currentTexture);

        this->setNodeDirty();
    }
    
    // gets the material for this billboard
    std::shared_ptr<imaterial> getMaterial()
    {
        if (_materials.get() == nullptr)
        {
            BAJA_THROW(std::logic_error("")); // ERROR:
        }

        return _materials->getMaterial(0);
    }

    
    // gets the material collection for this billboard
    std::shared_ptr<imaterial_collection> getMaterialCollection()
    {
        return _materials;
    }
    
    // gets the texture for this billboard
    std::shared_ptr<itexture> getTexture()
    {
        std::shared_ptr<imaterial> material = this->getMaterial();
        return material->getTexture();
    }
    
    // gets the specified texture for this billboard
    std::shared_ptr<itexture> getTextureAtIndex(uint32 index)
    {
        std::shared_ptr<imaterial> material;
        material = this->getMaterial();
        return material->getTextureAtIndex(index);
    }
    
    // sets the texture for this billboard
    void setTexture(const std::shared_ptr<itexture>& texture)
    {
        std::shared_ptr<imaterial> material;
        material = this->getMaterial();

        material->setTexture(texture);
    }

    // sets the texture at a given index for this billboard
    void setTextureAtIndex(uint32 index, const std::shared_ptr<itexture>& texture)
    {
        std::shared_ptr<imaterial> material;
        material = this->getMaterial();

        material->setTextureAtIndex(index, texture);
    }
    
    // initializes the billboard
    void init(
        const std::shared_ptr<iengine>& graphics,
        const float32 width,
        const float32 height,
        const uint32 horzTileCount,
        const uint32 vertTileCount
        )
    {
        // forget about any current mesh & material we were using
        _mesh.reset(); // TEST:
        _materials.reset();

        BAJA_CHECK_ARG(horzTileCount != 0);
        BAJA_CHECK_ARG(vertTileCount != 0);

        uint32 vertexCountWidth = horzTileCount + 1;
        uint32 vertexCountHeight = vertTileCount + 1;
        uint32 totalVerts = vertexCountWidth * vertexCountHeight;

        // create vertex buffer data
        std::vector<vertex_lit_colored_textured> vertexBuffer;
        vertexBuffer.resize(totalVerts);
        
        // set vertex buffer data
        
        // billboard mesh will be the width and length specified in this function, in X and Y.
        // the billboard mesh lies flat on the Z plane.
        
        vertex_lit_colored_textured* vbData = vertexBuffer.data();

        m::vector3d billboardNormal(0.0f, 0.0f, 1.0f);

        float32 halfWidth = width/2.0f;
        float32 halfHeight = height/2.0f;
        
        float32 colStep = 1.0f / horzTileCount;
        float32 rowStep = 1.0f / vertTileCount;

        for (uint32 row = 0; row < vertexCountHeight; row++)
        {
            float32 yLerp = (row == vertTileCount ? 1.0f : row * rowStep);
            uint32 rowOffset = row * vertexCountWidth;

            for(uint32 col = 0; col < vertexCountWidth; col++)
            {
                float32 xLerp = (col == horzTileCount ? 1.0f : col * colStep);
                uint32 finalOffset = rowOffset + col;

                vbData[finalOffset].position.x = (xLerp * width) - halfWidth;
                vbData[finalOffset].position.y = -((yLerp * height) - halfHeight);
                vbData[finalOffset].position.z = 0.0f;
                vbData[finalOffset].normal = billboardNormal;
                vbData[finalOffset].color = 0xffffffff;
                vbData[finalOffset].uv[0].u = xLerp;
                vbData[finalOffset].uv[0].v = yLerp;
            }
        }

        // create index buffer data
        uint32 indicesNeededPerRow = (vertexCountWidth * 2) + 2;
        uint32 totalIndices = indicesNeededPerRow * vertTileCount - 1; // minus one, since first row indices don't start "stitched together"
        std::vector<uint16> indexBuffer;
        indexBuffer.resize(totalIndices);

        // set index buffer data
        uint16* index = indexBuffer.data();

        for(uint16 row = 0; row < vertTileCount; row++)
        {
            uint16 startVert = (uint16)(row * vertexCountWidth);
            uint16 endVert = (uint16)(startVert + vertexCountWidth - 1);

            // we add extra index at start and end so we can stitch together
            // the triangle strips of each row, but not for the first row
            if (row != 0)
            {
                *index = (uint16)(startVert + vertexCountWidth);
                index++;
            }

            for(uint16 vert = startVert; vert <= endVert; vert++)
            {
                *index = (uint16)(vert + vertexCountWidth);
                index++;
                *index = vert;
                index++;
            }

            // add extra duplicate index to help stitch triangle strips together
            *index = endVert;
            index++;
        }

        // create the mesh
        _mesh = createMesh(
            graphics,
            vertexBuffer.data(),
            totalVerts,
            indexBuffer.data(),
            totalIndices,
            primitive_type::triangleStrip,
            totalIndices-2
            );

        // clone material collection
        _materials = _mesh->cloneMaterialCollection();

        this->setNodeDirty();

        // init members
        _width = width;
        _height = height;
        _horzTileCount = horzTileCount;
        _vertTileCount = vertTileCount;
    }
    
    // render the billboard
    void renderInternal( 
        const std::shared_ptr<iengine>& graphics,
        const m::matrix44* localToWorld
        )
    {
        bool pushedMatrix = false;

        if (localToWorld == nullptr)
        {
            // we are using matrix stack to properly transform matrices
            m::matrix44 mat;
            this->getLocalToParent(&mat);
            graphics->pushWorldMatrix(mat, true);
            pushedMatrix = true;
        }

        if (_mesh.get() != nullptr)
        {
            if (this->getEffect().get() == nullptr)
            {
                _mesh->render(
                    graphics, 
                    _materials,
                    localToWorld, 
                    this->getDefaultEffect()
                    );
            }
            else
            {
                _mesh->renderWithEffect(
                    graphics, 
                    _materials,
                    localToWorld,
                    this->getEffect()
                    );
            }
        }

        if (pushedMatrix)
        {
            (void)graphics->popWorldMatrix();
        }
    }
    
    // performs hit testing from pixel space to the billboard node
    bool pixelHitTest(
        const std::shared_ptr<iscene_transformer>& sceneTransformer,
        const m::point2d& pixel,
        m::point3d* hitPositionLocalSpace
        )
    {
        // assume we won't intersect
        bool hitTestPassed = false;
        if (this->getIsVisible())
        {
            m::plane3d billboardPlane(m::point3d(0.0f,0.0f,0.0f), m::vector3d(0.0f,0.0f,1.0f));
            m::line3d worldLine;
            m::line3d localLine;

            float32 halfWidth = _width/2.0f;
            float32 halfHeight = _height/2.0f;
            float32 t;

            // convert pixel to a world space line segment (from near to back clipping planes)
            worldLine = sceneTransformer->pixelToWorldSpace(pixel);

            // transform into local space
            this->getWorldToLocal().apply(worldLine, &localLine);

            // if intersects, continue
            if (localLine.intersects(billboardPlane, &t))
            {
                m::point3d intersectionPoint;
                localLine.getPointAt(t, &intersectionPoint);

                // remember hit position in local space if param is non-nullptr
                if (hitPositionLocalSpace != nullptr)
                {
                    hitPositionLocalSpace->x = intersectionPoint.x;
                    hitPositionLocalSpace->y = intersectionPoint.y;
                    hitPositionLocalSpace->z = intersectionPoint.z;
                }

                // we hit if point is within our billboard width & height
                if (intersectionPoint.x >= -halfWidth && intersectionPoint.x <= halfWidth &&
                    intersectionPoint.y >= -halfHeight && intersectionPoint.y <= halfHeight)
                {
                    hitTestPassed = true;
                }
            }
        }

        return hitTestPassed;
    }

private:
    std::shared_ptr<imesh> _mesh;
    std::shared_ptr<imaterial_collection> _materials;

    float32 _width;
    float32 _height;

    uint32 _horzTileCount;
    uint32 _vertTileCount;
};

// Creates an Ibillboard_node

inline std::shared_ptr<ibillboard_node> createBillboardNode(
    const std::shared_ptr<iengine> graphics,
    const float32 width, 
    const float32 height, 
    const uint32 horzTileCount,
    const uint32 vertTileCount
    )
{
    std::shared_ptr<billboard_node> returnValue = billboard_node::create(graphics, width, height, horzTileCount, vertTileCount);
    return std::dynamic_pointer_cast<ibillboard_node>(returnValue);
}

}}} // namespace baja::graphics::scenes
